package org.hibernate.cfg;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.Access;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Transient;

import org.hibernate.AnnotationException;
import org.hibernate.MappingException;
import org.hibernate.annotations.ManyToAny;
import org.hibernate.annotations.Target;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.jaxb.Origin;
import org.hibernate.boot.jaxb.SourceType;
import org.hibernate.cfg.annotations.HCANNHelper;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.jboss.logging.Logger;

/**
 * @author zhengqiang
 * @description 重写hibernate的PropertyContainer类,实现生成表的字段顺序和类的字段声明顺序一致
 */
class PropertyContainer {

	private static final CoreMessageLogger LOG = Logger.getMessageLogger(CoreMessageLogger.class, PropertyContainer.class.getName());

	private final XClass xClass;
	private final XClass entityAtStake;

	private final AccessType classLevelAccessType;

	private final LinkedHashMap<String, XProperty> persistentAttributeMap;

	PropertyContainer(XClass clazz, XClass entityAtStake, AccessType defaultClassLevelAccessType) {
		this.xClass = clazz;
		this.entityAtStake = entityAtStake;

		if (defaultClassLevelAccessType == AccessType.DEFAULT) {
			defaultClassLevelAccessType = AccessType.PROPERTY;
		}

		AccessType localClassLevelAccessType = determineLocalClassDefinedAccessStrategy();
		assert localClassLevelAccessType != null;

		this.classLevelAccessType = localClassLevelAccessType != AccessType.DEFAULT ? localClassLevelAccessType : defaultClassLevelAccessType;
		assert classLevelAccessType == AccessType.FIELD || classLevelAccessType == AccessType.PROPERTY;

		this.persistentAttributeMap = new LinkedHashMap<String, XProperty>();

		final List<XProperty> fields = xClass.getDeclaredProperties(AccessType.FIELD.getType());
		final List<XProperty> getters = xClass.getDeclaredProperties(AccessType.PROPERTY.getType());

		preFilter(fields, getters);

		final Map<String, XProperty> persistentAttributesFromGetters = new HashMap<String, XProperty>();

		collectPersistentAttributesUsingLocalAccessType(persistentAttributeMap, persistentAttributesFromGetters, fields, getters);
		collectPersistentAttributesUsingClassLevelAccessType(persistentAttributeMap, persistentAttributesFromGetters, fields, getters);
	}

	private void preFilter(List<XProperty> fields, List<XProperty> getters) {
		Iterator<XProperty> propertyIterator = fields.iterator();
		while (propertyIterator.hasNext()) {
			final XProperty property = propertyIterator.next();
			if (mustBeSkipped(property)) {
				propertyIterator.remove();
			}
		}

		propertyIterator = getters.iterator();
		while (propertyIterator.hasNext()) {
			final XProperty property = propertyIterator.next();
			if (mustBeSkipped(property)) {
				propertyIterator.remove();
			}
		}
	}

	private void collectPersistentAttributesUsingLocalAccessType(LinkedHashMap<String, XProperty> persistentAttributeMap, Map<String, XProperty> persistentAttributesFromGetters, List<XProperty> fields, List<XProperty> getters) {

		// Check fields...
		Iterator<XProperty> propertyIterator = fields.iterator();
		while (propertyIterator.hasNext()) {
			final XProperty xProperty = propertyIterator.next();
			final Access localAccessAnnotation = xProperty.getAnnotation(Access.class);
			if (localAccessAnnotation == null || localAccessAnnotation.value() != javax.persistence.AccessType.FIELD) {
				continue;
			}

			propertyIterator.remove();
			persistentAttributeMap.put(xProperty.getName(), xProperty);
		}

		// Check getters...
		propertyIterator = getters.iterator();
		while (propertyIterator.hasNext()) {
			final XProperty xProperty = propertyIterator.next();
			final Access localAccessAnnotation = xProperty.getAnnotation(Access.class);
			if (localAccessAnnotation == null || localAccessAnnotation.value() != javax.persistence.AccessType.PROPERTY) {
				continue;
			}

			propertyIterator.remove();

			final String name = xProperty.getName();

			// HHH-10242 detect registration of the same property getter twice -
			// eg boolean isId() + UUID getId()
			final XProperty previous = persistentAttributesFromGetters.get(name);
			if (previous != null) {
				throw new org.hibernate.boot.MappingException(LOG.ambiguousPropertyMethods(xClass.getName(), HCANNHelper.annotatedElementSignature(previous),
						HCANNHelper.annotatedElementSignature(xProperty)), new Origin(SourceType.ANNOTATION, xClass.getName()));
			}

			persistentAttributeMap.put(name, xProperty);
			persistentAttributesFromGetters.put(name, xProperty);
		}
	}

	private void collectPersistentAttributesUsingClassLevelAccessType(LinkedHashMap<String, XProperty> persistentAttributeMap, Map<String, XProperty> persistentAttributesFromGetters,
			List<XProperty> fields, List<XProperty> getters) {
		if (classLevelAccessType == AccessType.FIELD) {
			for (XProperty field : fields) {
				if (persistentAttributeMap.containsKey(field.getName())) {
					continue;
				}
				persistentAttributeMap.put(field.getName(), field);
			}
		} else {
			for (XProperty getter : getters) {
				final String name = getter.getName();

				// HHH-10242 detect registration of the same property getter
				// twice - eg boolean isId() + UUID getId()
				final XProperty previous = persistentAttributesFromGetters.get(name);
				if (previous != null) {
					throw new org.hibernate.boot.MappingException(LOG.ambiguousPropertyMethods(xClass.getName(), HCANNHelper.annotatedElementSignature(previous),
							HCANNHelper.annotatedElementSignature(getter)), new Origin(SourceType.ANNOTATION, xClass.getName()));
				}

				if (persistentAttributeMap.containsKey(name)) {
					continue;
				}

				persistentAttributeMap.put(getter.getName(), getter);
				persistentAttributesFromGetters.put(name, getter);
			}
		}
	}

	public XClass getEntityAtStake() {
		return entityAtStake;
	}

	public XClass getDeclaringClass() {
		return xClass;
	}

	public AccessType getClassLevelAccessType() {
		return classLevelAccessType;
	}

	public Collection<XProperty> getProperties() {
		assertTypesAreResolvable();
		return Collections.unmodifiableCollection(persistentAttributeMap.values());
	}

	private void assertTypesAreResolvable() {
		for (XProperty xProperty : persistentAttributeMap.values()) {
			if (!xProperty.isTypeResolved() && !discoverTypeWithoutReflection(xProperty)) {
				String msg = "Property " + StringHelper.qualify(xClass.getName(), xProperty.getName()) + " has an unbound type and no explicit target entity. Resolve this Generic usage issue"
						+ " or set an explicit target attribute (eg @OneToMany(target=) or use an explicit @Type";
				throw new AnnotationException(msg);
			}
		}
	}

	//
	// private void considerExplicitFieldAndPropertyAccess() {
	// for ( XProperty property : fieldAccessMap.values() ) {
	// Access access = property.getAnnotation( Access.class );
	// if ( access == null ) {
	// continue;
	// }
	//
	// // see "2.3.2 Explicit Access Type" of JPA 2 spec
	// // the access type for this property is explicitly set to
	// AccessType.FIELD, hence we have to
	// // use field access for this property even if the default access type for
	// the class is AccessType.PROPERTY
	// AccessType accessType = AccessType.getAccessStrategy( access.value() );
	// if (accessType == AccessType.FIELD) {
	// propertyAccessMap.put(property.getName(), property);
	// }
	// else {
	// LOG.debug(
	// "Placing @Access(AccessType.FIELD) on a field does not have any effect."
	// );
	// }
	// }
	//
	// for ( XProperty property : propertyAccessMap.values() ) {
	// Access access = property.getAnnotation( Access.class );
	// if ( access == null ) {
	// continue;
	// }
	//
	// AccessType accessType = AccessType.getAccessStrategy( access.value() );
	//
	// // see "2.3.2 Explicit Access Type" of JPA 2 spec
	// // the access type for this property is explicitly set to
	// AccessType.PROPERTY, hence we have to
	// // return use method access even if the default class access type is
	// AccessType.FIELD
	// if (accessType == AccessType.PROPERTY) {
	// fieldAccessMap.put(property.getName(), property);
	// }
	// else {
	// LOG.debug(
	// "Placing @Access(AccessType.PROPERTY) on a field does not have any effect."
	// );
	// }
	// }
	// }

	// /**
	// * Retrieves all properties from the {@code xClass} with the specified
	// access type. This method does not take
	// * any jpa access rules/annotations into account yet.
	// *
	// * @param access The access type - {@code AccessType.FIELD} or {@code
	// AccessType.Property}
	// *
	// * @return A maps of the properties with the given access type keyed
	// against their property name
	// */
	// private LinkedHashMap<String, XProperty> initProperties(AccessType
	// access) {
	// if ( !( AccessType.PROPERTY.equals( access ) || AccessType.FIELD.equals(
	// access ) ) ) {
	// throw new IllegalArgumentException(
	// "Access type has to be AccessType.FIELD or AccessType.Property" );
	// }
	//
	// //order so that property are used in the same order when binding native
	// query
	// LinkedHashMap<String, XProperty> propertiesMap = new
	// LinkedHashMap<String, XProperty>();
	// List<XProperty> properties = xClass.getDeclaredProperties(
	// access.getType() );
	// for ( XProperty property : properties ) {
	// if ( mustBeSkipped( property ) ) {
	// continue;
	// }
	// // HHH-10242 detect registration of the same property twice eg boolean
	// isId() + UUID getId()
	// XProperty oldProperty = propertiesMap.get( property.getName() );
	// if ( oldProperty != null ) {
	// throw new org.hibernate.boot.MappingException(
	// LOG.ambiguousPropertyMethods(
	// xClass.getName(),
	// HCANNHelper.annotatedElementSignature( oldProperty ),
	// HCANNHelper.annotatedElementSignature( property )
	// ),
	// new Origin( SourceType.ANNOTATION, xClass.getName() )
	// );
	// }
	//
	// propertiesMap.put( property.getName(), property );
	// }
	// return propertiesMap;
	// }

	@SuppressWarnings("deprecation")
	private AccessType determineLocalClassDefinedAccessStrategy() {
		AccessType classDefinedAccessType;

		AccessType hibernateDefinedAccessType = AccessType.DEFAULT;
		AccessType jpaDefinedAccessType = AccessType.DEFAULT;

		org.hibernate.annotations.AccessType accessType = xClass.getAnnotation(org.hibernate.annotations.AccessType.class);
		if (accessType != null) {
			hibernateDefinedAccessType = AccessType.getAccessStrategy(accessType.value());
		}

		Access access = xClass.getAnnotation(Access.class);
		if (access != null) {
			jpaDefinedAccessType = AccessType.getAccessStrategy(access.value());
		}

		if (hibernateDefinedAccessType != AccessType.DEFAULT && jpaDefinedAccessType != AccessType.DEFAULT && hibernateDefinedAccessType != jpaDefinedAccessType) {
			throw new MappingException("@AccessType and @Access specified with contradicting values. Use of @Access only is recommended. ");
		}

		if (hibernateDefinedAccessType != AccessType.DEFAULT) {
			classDefinedAccessType = hibernateDefinedAccessType;
		} else {
			classDefinedAccessType = jpaDefinedAccessType;
		}
		return classDefinedAccessType;
	}

	private static boolean discoverTypeWithoutReflection(XProperty p) {
		if (p.isAnnotationPresent(OneToOne.class) && !p.getAnnotation(OneToOne.class).targetEntity().equals(void.class)) {
			return true;
		} else if (p.isAnnotationPresent(OneToMany.class) && !p.getAnnotation(OneToMany.class).targetEntity().equals(void.class)) {
			return true;
		} else if (p.isAnnotationPresent(ManyToOne.class) && !p.getAnnotation(ManyToOne.class).targetEntity().equals(void.class)) {
			return true;
		} else if (p.isAnnotationPresent(ManyToMany.class) && !p.getAnnotation(ManyToMany.class).targetEntity().equals(void.class)) {
			return true;
		} else if (p.isAnnotationPresent(org.hibernate.annotations.Any.class)) {
			return true;
		} else if (p.isAnnotationPresent(ManyToAny.class)) {
			if (!p.isCollection() && !p.isArray()) {
				throw new AnnotationException("@ManyToAny used on a non collection non array property: " + p.getName());
			}
			return true;
		} else if (p.isAnnotationPresent(Type.class)) {
			return true;
		} else if (p.isAnnotationPresent(Target.class)) {
			return true;
		}
		return false;
	}

	private static boolean mustBeSkipped(XProperty property) {
		// provider?)
		return property.isAnnotationPresent(Transient.class) || "net.sf.cglib.transform.impl.InterceptFieldCallback".equals(property.getType().getName()) || "org.hibernate.bytecode.internal.javassist.FieldHandler".equals(property.getType().getName());
	}
}
